P03-2. 2세대 Ambient Mesh 혁명 - ztunnel과 Waypoint - 실습 검증

Part 03 (실습): 2세대 Ambient Mesh 혁명 — ztunnel과 Waypoint 검증

실습 목표

  1. 클러스터에 쿠버네티스 표준 Gateway API (v1.6.3) 를 설치하고, Istio 프로필을 1세대 default에서 2세대 ambient 로 전환하여 istio-cni-node 와 ztunnel 데몬셋(DaemonSet)을 기동한다.
  2. part1-demo 파드들에서 사이드카를 완전히 제거해 순수 애플리케이션 컨테이너(READY 1/1)로 만든 뒤, 파드를 재시작하지 않고 네임스페이스 라벨 하나만으로 즉시 Ambient Mesh(HBONE)에 편입시킨다.
  3. 워커 노드에서 nsenter로 1/1 파드의 네트워크 방(NetNS)을 열어, 사이드카가 없는데도 노드의 ztunnel 프로세스가 파드 방 안쪽에 직접 15001, 15006, 15008 소켓을 열어 둔 현장(In-Pod Redirection) 을 포착한다.
  4. L7 기능을 담당하는 Waypoint Proxy 를 배포하고, ztunnel 단독일 때(L4)와 Waypoint 경유일 때(L7)의 로드밸런싱 및 HBONE(:15008) 터널링 로그 차이를 완벽히 비교한다.

사전 환경 확인

  • P03-1 이론 개념을 먼저 읽고 오면 ztunnel, istio-cni-node, HBONE(:15008), Waypoint Proxy의 역할 분담이 한눈에 이해됩니다.
  • Windows PowerShell 창을 열고 아래 명령어로 docker-desktop 3노드 클러스터 상태를 점검합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl get nodes
    


실습 아키텍처 및 검증 흐름 한눈에 보기

flowchart TB
    subgraph STEP1_3["Step 1~3 · 사이드카 제거 (READY 1/1) 및 Ambient L4 모드 활성화"]
        direction LR
        C1["study-client (READY 1/1)
desktop-worker"] -->|"파드 내부 소켓(:15001)"| Z1["Node 1 ztunnel"] Z1 ==>|"HBONE 터널 (:15008)
mTLS + SPIFFE 인증"| Z2["Node 2 ztunnel"] Z2 -->|"평문 전달(:8080)"| S1["order-api (READY 1/1)
desktop-worker2"] end subgraph STEP5["Step 5 · L7 Waypoint Proxy 배포 후 2-Hop HBONE 흐름"] direction LR C2["study-client (READY 1/1)"] --> Z_A["Client ztunnel"] Z_A ==>|"1차 HBONE (:15008)
Service VIP (10.96.x.x:80)"| WP["Waypoint Proxy (Envoy L7)
Keep-Alive 요청별 1:1 분산!"] WP ==>|"2차 HBONE (:15008)
개별 파드 IP (:8080)"| Z_B["Server ztunnel"] Z_B --> S2["order-api 파드 1, 2"] end

Step 1. Kubernetes Gateway API (v1.6.3) 및 Istio ambient 프로필 설치

2세대 Ambient Mesh에서는 L7 Waypoint Proxy를 만들고 제어할 때 쿠버네티스 공식 표준인 Gateway API 를 사용합니다. 먼저 Gateway API 표준 CRD(v1.6.3)를 클러스터에 등록한 뒤, Istio를 ambient 프로필로 설치(전환)합니다.

# 1. Kubernetes 공식 Gateway API v1.6.3 표준 CRD 설치 (--server-side 옵션 필수)
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml

# 2. Istio를 2세대 Ambient Mesh 프로필로 설치 (기존 default 프로필에서 자동 전환됨)
istioctl install --set profile=ambient --skip-confirmation

# 3. istio-system 네임스페이스의 파드 및 DaemonSet 확인
kubectl wait --for=condition=Ready pod --all -n istio-system --timeout=120s
kubectl get pods -n istio-system -o wide
kubectl get daemonset -n istio-system

실제 출력 결과:

customresourcedefinition.apiextensions.k8s.io/backendtlspolicies.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gatewayclasses.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/gateways.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/grpcroutes.gateway.networking.k8s.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/httproutes.gateway.networking.k8s.io serverside-applied
...
- Processing resources for Istio core.
✔ Istio core installed ⛵️
- Processing resources for CNI, Istiod.
✔ Istiod installed 🧠
✔ CNI installed 🪢
- Processing resources for Ztunnel.
✔ Ztunnel installed 🔒
✔ Installation completeThe ambient profile has been installed successfully, enjoy Istio without sidecars!

NAME                             READY   STATUS    RESTARTS   AGE   IP           NODE                    NOMINATED NODE   READINESS GATES
istio-cni-node-75dxr             1/1     Running   0          41s   10.244.2.4   desktop-worker          <none>           <none>
istio-cni-node-cfj65             1/1     Running   0          41s   10.244.0.5   desktop-control-plane   <none>           <none>
istio-cni-node-q45j2             1/1     Running   0          41s   10.244.1.6   desktop-worker2         <none>           <none>
istiod-6fd78d8666-v5grw          1/1     Running   0          41s   10.244.1.5   desktop-worker2         <none>           <none>
ztunnel-9x97z                    1/1     Running   0          14s   10.244.1.7   desktop-worker2         <none>           <none>
ztunnel-cg7xq                    1/1     Running   0          14s   10.244.0.6   desktop-control-plane   <none>           <none>
ztunnel-zb2gf                    1/1     Running   0          14s   10.244.2.5   desktop-worker          <none>           <none>

NAME             DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
istio-cni-node   3         3         3       3            3           kubernetes.io/os=linux   42s
ztunnel          3         3         3       3            3           kubernetes.io/os=linux   15s

출력 결과 상세 해부:


Step 2. 1세대 사이드카 제거 (2/2 → 1/1) 및 무중단 Ambient 모드 전환!

이제 part1-demo 네임스페이스에서 1세대 사이드카 자동 주입 라벨(istio-injection=enabled)을 지우고 파드들을 재배포하여, 아무런 사이드카도 없는 순수 파드 상태(READY 1/1) 로 되돌립니다.
그다음 파드를 단 한 개도 재시작하지 않고 네임스페이스 라벨만 붙여 Ambient Mesh로 편입되는 기적을 확인합니다!

# 1. 1세대 사이드카 주입 라벨 제거 (키 이름 뒤에 하이픈 '-'을 붙이면 라벨이 삭제됨)
kubectl label namespace part1-demo istio-injection-

# 2. 기존 2/2 파드들을 다시 띄워 순수 1/1 파드로 초기화
kubectl delete pod study-client -n part1-demo --ignore-not-found
kubectl rollout restart deployment/order-api -n part1-demo
kubectl apply -f "$HOME\mesh-study\order-api.yaml"
kubectl rollout status deployment/order-api -n part1-demo
kubectl wait --for=condition=Ready pod --all -n part1-demo --timeout=120s

# 3. 모든 파드가 사이드카 없이 READY 1/1 인지 확인!
kubectl get pods -n part1-demo -o wide

실제 출력 결과:

NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE              NOMINATED NODE   READINESS GATES
order-api-86d8bb68dd-mkvrw   1/1     Running   0          33s   10.244.2.6   desktop-worker    <none>           <none>
order-api-86d8bb68dd-xh5rm   1/1     Running   0          29s   10.244.1.8   desktop-worker2   <none>           <none>
study-client                 1/1     Running   0          8s    10.244.2.7   desktop-worker    <none>           <none>

모든 파드가 READY 1/1 로 깔끔하게 돌아왔습니다!
이제 파드를 재시작하지 않은 채로 네임스페이스에 istio.io/dataplane-mode=ambient 라벨을 붙이고, ztunnel의 장부(istioctl ztunnel-config workloads)를 조회해 봅니다.

주의: istioctl ztunnel-config 명령어의 네임스페이스 옵션

istioctl ztunnel-config workloads에서 -n(--namespace) 옵션은 "ztunnel 데몬셋 자체가 설치된 네임스페이스(istio-system)" 를 뜻합니다. 우리가 조회하고 싶은 애플리케이션 파드의 네임스페이스로 필터링할 때는 반드시 --workload-namespace part1-demo 옵션을 사용해야 합니다!

# 1. 네임스페이스에 Ambient 모드 활성화 라벨 부착 (파드 재시작 없음!)
kubectl label namespace part1-demo istio.io/dataplane-mode=ambient --overwrite

# 2. ztunnel이 인식한 part1-demo 파드들의 통신 프로토콜(PROTOCOL) 확인!
istioctl ztunnel-config workloads --workload-namespace part1-demo

실제 출력 결과:

namespace/part1-demo labeled

NAMESPACE  POD NAME                   ADDRESS    NODE            WAYPOINT PROTOCOL
part1-demo order-api-86d8bb68dd-mkvrw 10.244.2.6 desktop-worker  None     HBONE
part1-demo order-api-86d8bb68dd-xh5rm 10.244.1.8 desktop-worker2 None     HBONE
part1-demo study-client               10.244.2.7 desktop-worker  None     HBONE

Step 3. 커널 NetNS 침투(nsenter) — 1/1 파드 방 안에 열린 ztunnel 소켓 실측!

P03-1 이론 노트에서 배운 "파드는 1/1인데 노드의 ztunnel 프로세스가 파드의 네트워크 방(NetNS) 안쪽에 직접 자기 소켓(15001, 15006, 15008)을 열어 둔다" 는 말이 사실인지, 지금 바로 워커 노드에서 nsenter로 study-client 파드 방 문을 열고 들어가 소켓 목록(ss -tlpn)을 찍어 봅니다!

# study-client가 떠 있는 워커 노드에서 sleep 프로세스의 NetNS 안으로 들어가 열려 있는 TCP 소켓(ss -tlpn) 확인
$clientNode = kubectl get pod study-client -n part1-demo -o jsonpath="{.spec.nodeName}"
docker exec $clientNode sh -c 'nsenter -t $(pidof sleep) -n ss -tlpn'

실제 출력 결과:

State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess                            
LISTEN 0      128        127.0.0.1:15053      0.0.0.0:*    users:(("ztunnel",pid=1658,fd=25))
LISTEN 0      128                *:15006            *:*    users:(("ztunnel",pid=1658,fd=30))
LISTEN 0      128                *:15001            *:*    users:(("ztunnel",pid=1658,fd=31))
LISTEN 0      128                *:15008            *:*    users:(("ztunnel",pid=1658,fd=29))
LISTEN 0      128            [::1]:15053         [::]:*    users:(("ztunnel",pid=1658,fd=27))
소름 돋는 실측 증거: users:(("ztunnel",pid=1658,fd=...))

분명히 study-client 파드 안에는 sleep 3650d 컨테이너 딱 1개(READY 1/1)밖에 없습니다.
그런데 study-client 파드의 네트워크 방(NetNS) 안에서 열려 있는 포트를 조회해 보니, 노드 데몬셋인 ztunnel(pid=1658)이 파드 방 안쪽에 직접 파일 디스크립터(fd=25, 29, 30, 31)를 꽂아 넣고 다음 4개의 귀를 열어 둔 것이 눈으로 확인됩니다!

  • *:15001: 파드 밖으로 나가는(Outbound) 패킷을 가로채는 귀
  • *:15006: 파드로 들어오는 평문(Inbound Passthrough) 패킷을 받는 귀
  • *:15008: 다른 노드의 ztunnel이나 Waypoint가 보내는 암호화된 HBONE 터널 패킷을 받는 귀
  • 127.0.0.1:15053: 파드의 DNS 질의를 가로채 빠르게 응답해 주는 내장 DNS 프록시 귀

Step 4. 워커 노드 간 HBONE(:15008) mTLS 암호화 통신 로그 확인

이제 study-client에서 order-api로 HTTP 요청을 2번 보내고, ztunnel이 남긴 실시간 액세스 로그를 확인합니다. (이때 study-client에는 컨테이너가 1개뿐이므로 더 이상 귀찮게 -c study-client 옵션을 붙일 필요도 없습니다!)

# 1. study-client에서 order-api로 Keep-Alive 요청 2회 전송
kubectl exec -n part1-demo study-client -- curl -s --http1.1 "http://order-api/ambient-test?[1-2]" | Select-String '"hostname"'

# 2. ztunnel 데몬셋 로그에서 HBONE(:15008) 터널링 기록 확인
kubectl logs -n istio-system -l app=ztunnel --tail=10 | Select-String "connection complete"

실제 출력 결과:

  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-xh5rm"
  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-xh5rm"

2026-10-25T07:05:06.210046Z	info	access	connection complete	src.addr=10.244.2.7:56424 src.workload="study-client" src.namespace="part1-demo" src.identity="spiffe://cluster.local/ns/part1-demo/sa/default" src.cluster="Kubernetes" dst.addr=10.244.1.8:15008 dst.hbone_addr=10.244.1.8:8080 dst.service="order-api.part1-demo.svc.cluster.local" dst.workload="order-api-86d8bb68dd-xh5rm" dst.namespace="part1-demo" dst.identity="spiffe://cluster.local/ns/part1-demo/sa/default" dst.cluster="Kubernetes" direction="inbound" bytes_sent=1304 bytes_recv=174 duration="61ms"
2026-10-25T07:05:06.210527Z	info	access	connection complete	src.addr=10.244.2.7:56890 src.workload="study-client" src.namespace="part1-demo" src.identity="spiffe://cluster.local/ns/part1-demo/sa/default" src.cluster="Kubernetes" dst.addr=10.244.1.8:15008 dst.hbone_addr=10.244.1.8:8080 dst.service="order-api.part1-demo.svc.cluster.local" dst.workload="order-api-86d8bb68dd-xh5rm" dst.namespace="part1-demo" dst.identity="spiffe://cluster.local/ns/part1-demo/sa/default" dst.cluster="Kubernetes" direction="outbound" bytes_sent=174 bytes_recv=1304 duration="63ms"

로그 핵심 포인트 2가지:

  1. dst.addr=10.244.1.8:15008 & dst.hbone_addr=10.244.1.8:8080:
    desktop-worker 노드의 study-client(10.244.2.7)에서 출발한 패킷이 다른 노드인 desktop-worker2의 order-api(10.244.1.8)로 날아갈 때, 실제 네트워크 위에서는 15008번 포트(HBONE mTLS 터널) 로 암호화되어 날아갔고, 터널 안쪽 헤더(dst.hbone_addr)에 원래 목적지인 :8080 포트가 담겨 전달되었습니다!
  2. 잠깐! 왜 두 번의 요청이 모두 xh5rm 파드 하나로만 갔을까요?:
    ztunnel은 순수 L4(TCP) 프록시이기 때문입니다! HTTP 본문을 뜯어보지 않으므로, 하나의 Keep-Alive TCP 연결로 들어온 요청 2개는 처음 맺은 HBONE 터널(xh5rm) 하나로 계속 흘러갑니다.
    그렇다면 Keep-Alive 연결 위에서도 매 HTTP 요청마다 파드를 번갈아 골라주는 L7 로드밸런싱을 쓰려면 어떻게 해야 할까요? 바로 Step 5에서 Waypoint Proxy를 켜면 됩니다!

Step 5. L7 Waypoint Proxy 배포 및 2-Hop HBONE 로드밸런싱 검증

istioctl waypoint apply 명령어로 part1-demo 네임스페이스에 L7 전담 프록시인 waypoint 를 배포하고 등록(--enroll-namespace)합니다.

# 1. part1-demo 네임스페이스에 Waypoint 프록시 생성 및 자동 등록
istioctl waypoint apply -n part1-demo --enroll-namespace --wait

# 2. 생성된 Kubernetes Gateway 리소스와 waypoint 파드 확인
kubectl get gtw,pods -n part1-demo -o wide

# 3. ztunnel의 서비스 장부에 order-api의 WAYPOINT가 연결되었는지 확인
istioctl ztunnel-config services --service-namespace part1-demo

실제 출력 결과:

✅ waypoint part1-demo/waypoint applied
✅ waypoint part1-demo/waypoint is ready!
✅ namespace part1-demo labeled with "istio.io/use-waypoint: waypoint"

NAME                                         CLASS            ADDRESS       PROGRAMMED   AGE
gateway.gateway.networking.k8s.io/waypoint   istio-waypoint   10.96.27.87   True         46s

NAME                             READY   STATUS    RESTARTS   AGE     IP           NODE              NOMINATED NODE   READINESS GATES
pod/order-api-86d8bb68dd-mkvrw   1/1     Running   0          2m43s   10.244.2.6   desktop-worker    <none>           <none>
pod/order-api-86d8bb68dd-xh5rm   1/1     Running   0          2m39s   10.244.1.8   desktop-worker2   <none>           <none>
pod/study-client                 1/1     Running   0          2m18s   10.244.2.7   desktop-worker    <none>           <none>
pod/waypoint-6cc777d4c7-cwlx2    1/1     Running   0          45s     10.244.2.8   desktop-worker    <none>           <none>

NAMESPACE  SERVICE NAME SERVICE VIP WAYPOINT ENDPOINTS
part1-demo order-api    10.96.72.69 waypoint 2/2
part1-demo waypoint     10.96.27.87 None     1/1

이제 다시 똑같이 Keep-Alive 연결 하나로 4번 연속 요청을 쏘고 응답 호스트네임과 ztunnel 로그를 확인해 봅니다.

# 1. Waypoint 경유 상태에서 Keep-Alive로 4회 연속 호출
kubectl exec -n part1-demo study-client -- curl -s --http1.1 "http://order-api/waypoint-test?[1-4]" | Select-String '"hostname"'

# 2. ztunnel 로그에서 study-client -> waypoint 로 향하는 1차 HBONE 터널 확인
kubectl logs -n istio-system -l app=ztunnel --tail=15 | Select-String "waypoint"

실제 출력 결과:

  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-xh5rm"
  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-mkvrw"
  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-mkvrw"
  "hostname": "order-api",
    "hostname": "order-api-86d8bb68dd-xh5rm"

2026-10-25T07:07:12.534873Z	info	access	connection complete	src.addr=10.244.2.7:56260 src.workload="study-client" src.namespace="part1-demo" src.identity="spiffe://cluster.local/ns/part1-demo/sa/default" src.cluster="Kubernetes" dst.addr=10.244.2.8:15008 dst.hbone_addr=10.96.72.69:80 dst.service="order-api.part1-demo.svc.cluster.local" dst.workload="waypoint-6cc777d4c7-cwlx2" dst.namespace="part1-demo" dst.identity="spiffe://cluster.local/ns/part1-demo/sa/waypoint" dst.cluster="Kubernetes" direction="outbound" bytes_sent=352 bytes_recv=3315 duration="184ms"

Step 6. 자주 발생하는 문제와 해결 (Troubleshooting)

증상 / 에러 메시지 원인 해결 방법
istioctl ztunnel-config workloads -n part1-demo 실행 시 daemonsets.apps "ztunnel" not found in the "part1-demo" namespace 에러 -n 플래그는 ztunnel 데몬셋이 설치된 시스템 네임스페이스(istio-system)를 지정하는 옵션임 워크로드 네임스페이스를 지정할 때는 --workload-namespace part1-demo, 서비스는 --service-namespace part1-demo 옵션 사용
istioctl waypoint apply 실행 시 no matches for kind "Gateway" in version "gateway.networking.k8s.io/v1" 에러 클러스터에 Kubernetes 표준 Gateway API CRD가 아직 설치되지 않음 Step 1의 kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml 실행
네임스페이스에 istio-injection=enabled와 istio.io/dataplane-mode=ambient를 동시에 붙임 한 네임스페이스에 1세대 사이드카 주입과 2세대 Ambient 라벨을 동시에 켜면 충돌 발생 반드시 kubectl label ns <이름> istio-injection- 로 사이드카 라벨을 먼저 지운 뒤 Ambient 라벨 적용

Step 7. 이번 파트 핵심 명령어 정리 (Command Reference)

# 1. Gateway API 표준 CRD 설치 및 Istio Ambient 프로필 설치
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
istioctl install --set profile=ambient --skip-confirmation

# 2. 네임스페이스를 무중단으로 Ambient Mesh에 편입
kubectl label namespace part1-demo istio.io/dataplane-mode=ambient --overwrite

# 3. ztunnel이 관리 중인 파드(HBONE 프로토콜 여부) 및 서비스(Waypoint 연결 여부) 조회
istioctl ztunnel-config workloads --workload-namespace part1-demo
istioctl ztunnel-config services --service-namespace part1-demo

# 4. 네임스페이스에 L7 Waypoint Proxy 배포 및 자동 연결
istioctl waypoint apply -n part1-demo --enroll-namespace --wait

Step 8. 셀프 체크 (Self-Check Q&A)

Q1. 1세대 사이드카 메시에서 2세대 Ambient Mesh로 넘어왔을 때, 애플리케이션 파드의 READY 컨테이너 수와 파드 재시작 여부는 어떻게 달라지나요?

Q2. ztunnel끼리 패킷을 주고받을 때 사용하는 프로토콜의 이름과 포트 번호는 무엇이며, 내부적으로 어떤 기술을 사용하나요?

Q3. 왜 ztunnel만 있을 때는 HTTP/1.1 Keep-Alive 요청이 한쪽 파드로만 몰렸다가, Waypoint Proxy를 배포하자마자 두 파드로 골고루 나뉘었나요?


🔗 다음 파트로 이동

이제 우리 클러스터는 사이드카 없는 2세대 Ambient Mesh (ztunnel + Waypoint) 체제로 완벽히 전환되었습니다!
다음 Part 04에서는 방금 ztunnel 로그에서 스쳐 지나갔던 src.identity="spiffe://cluster.local/ns/part1-demo/sa/default" 전자 여권의 실체를 해부하고, 허가받지 않은 해커 파드(rogue-client)와 금지된 URL 경로(/admin)를 칼같이 차단하는 Zero-Trust 보안 실습으로 넘어갑니다.


네비게이션

이전 목록 다음
◀ Part 03 이론 목록으로 Part 04 이론 ▶